16 结构、联合和枚举

16.1 结构变量

结构:结构的特性与数组很不相同。

  • 结构的元素(成员)可能具有不同的类型
  • 每个结构成员都有名字
  • 为了选择特殊的结构成员需要知名结构成员的名字而不是它的位置

扩展:大多数语言都提供类似的特性,所以结构可能听起来很舒需。再其它语言中,经常把结构称为纪录(record),把结构的成员称为字段(field)。

16.1.1 结构变量的声明

语法:只声明不初始化(会非配内存但成员不会初始化)

1
2
3
4
struct{
成员类型 成员名称;
...
}实例变量1, 实例变量2, ...;

实例:结构实例化的变量具备以下特点

  • 成员在内存中是按照顺序存储的
  • 内部成员拥有单独的名字空间(name space)
1
2
3
4
5
6
7
8
9
10
11
12
13
//零件
struct{
int number;//零件编号
char name[NAME_LEN+1];//零件名称
int on_hand;//零件现有数量
}part1, part2;//同时用这种结构实例化了两个变量

//员工
struct{
char name[NAME_LEN+1];//姓名
int number;//工号
char sex;//性别
}employee1, employee2;

16.1.2 结构变量的初始化

语法:声明的同时初始化

1
2
3
4
5
6
7
struct{
成员1类型 成员1名称;
成员2类型 成员2名称;
...
}实例变量1 = {成员1值, 成员2值, ...},
实例变量2 = {成员1值, 成员2值, ...},
...;

规则:类似数组

  1. 用于结构初始化式的表达式必须是常量
  2. 初始化式可以短于它所初始化的结构,任何剩余的成员都用0作为它的初始值
1
2
3
4
5
6
struct{
int number;
char name[NAME_LEN+1];
int on, hand;
}part1 = {528, "Disk drive", 10},
part2 = {914, "Printer cable", 5};

16.1.3 对结构的操作

限制:不能用==!=判定两个结构是否相等或不等。

16.1.3.1 访问成员

左值:结构成员的值是左值

  • 可以出现在赋值运算的左侧
  • 作为自增或自减表达式的操作数
1
2
3
printf("Part number %d:", part1.number);
part1.number = 228;//可以出现在赋值运算的左侧
part1.on_hand++;//作为自增或自减表达式的操作数

逗号运算符:结构变量.成员名

  • 优先级和后缀++和后缀–相同(几乎高于所有其他运算符)
1
scanf("%d", &part1.on_hand);//.运算符优先级高于&

16.1.3.2 赋值运算

说明:数组不能用=运算符实现变量间数组内容的复制,但结构变量可以。
注意:只能用于同一个结构类型声明的的变量之间。
技巧:把需要复制的数组嵌在结构体内(作为成员)进行复制。

1
2
3
4
5
struct{
int a[10];
}a1, a2;

a1 = a2;//a1的存储空间中数据和a2相同,实现了复制

16.2 结构类型

说明:上一小结重点放在结构变量而不是结构类型本身上,这一节将重点观察结构类型。
命名结构类型:如果需要在程序的不同位置声明结构变量,上一节的“匿名结构”就行不通了。c语言提供了两种命名结构的方法

  1. 声明“结构标记”(结构用语链表时,只能声明“结构标记”)
  2. 使用typedef定义类型名

16.2.1 结构标记的声明

结构标记(structure tag):结构标记用于标记某种特定结构类型的名字。

1
2
3
4
5
struct  结构类型名{
成员1类型 成员1名称;
...
}[结构变量1, ...];//分号表示声明的结束,不能省略
struct 结构类型名 结构变量2, ...;//struct关键字不能省略,因为结构类型名不是有效的c语言类型名(原生的和typedef定义的才是)。
1
2
3
4
5
6
7
8
9
//不仅声明了标记part,而且声明了变量
struct part{
int number;
char name[NAME_LEN+1];
int on_hand;
};
struct part part1 = {528, "disk drive", 10};
struct part part2;
part2 = part1;

16.2.2 结构类型的定义

说明:用typedef来定义真正的类型名。

1
2
3
4
typedef struct{
成员1类型 成员1名称;
...
} 结构类型名;
1
2
3
4
5
6
typedef struct{
int number;
char name[NAME_LEN+1];
int on_hand;
} Part;//类型名的名字必须出现在定义的末尾,而不是在单词struct的后边
Part part1, part2;

16.2.3 结构类型的实际参数和返回值

缺点:带来一定系统开销,尤其是结构题很大的时候。给函数传递结构和从函数返回结构都要求使用结构中所有成员的副本。

技巧:有时用指向结构的指针来代替传递给函数(或函数返回)的结构本身是很明智的做法。

16.2.3.1 用作参数

1
2
3
4
5
6
7
8
9
// 定义
void print_part (struct part p) {
printf("part number: %d\n", p.number);
printf("Part name: %s\n", p.name);
printf("Quality on hand: %d \n", p.on_hand);
}

// 传参
print_part(part1);

16.2.3.2 用作返回值

1
2
3
4
5
6
7
8
9
// 定义
struct part build_part(int number, const char* name, int on_hand)
{

struct part p;
p.number = number;
strcpy(p.name, name);
p.on_hand = on_hand;
return p;
}

16.3 数组和结构的嵌套

16.3.1 嵌套的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义姓名
struct person_name {
char first[FIRST_NAME_LEN + 1];
char middle_initial;
char last[LAST_NAME_LEN + 1];
};

//定义学生
struct student {
struct person_name name; // 结构的成员可以是另一种结构体
int_id, age;
char sex;
} student1, student2;

strcpy(student1.name.first, "Fred");

16.3.2 结构数组

说明:结构可以作为数组的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*声明*/
// 声明结构数组
struct part inventory[100];

/*访问*/
// 访问结构数组中的结构
print_part(inventory[i]);

/*操作*/
// 为数组中的结构的成员赋值
inventory[i].number = 883;

// 将数组中的结构的成员(字符串)置空
inventory[i].name[0] = '\0';

16.3.3 结构数组的初始化

语法:类似二维数组的初始化,每个结构都拥有自己的大括号。
注意:每个结构值的内层大括号是可选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义结构:国家代码
struct dialog_code {
char *country;
int code;
};

// 初始化
const struct dialog_code country_codes[] = {
{
"Argentina", 54
},
{
"Bangladesh", 66
}
};

16.3.4 程序:维护零件数据库

16.3.4.1 编写

readline.h
1
2
3
4
5
6
7
8
9
10
11
12
#ifndef READLINE_H
#define READLINE_H
#endif
/**
* 读入一行
* 会跳过开头的空白符
*
* @param str 读入的内容
* @param n 字符串的内容
* @return 读入字符的个数
*/

int read_line(char str[], int n);
readline.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <ctype.h>
#include <stdio.h>
#include "readline.h"

/**
* 读入一行
* 会跳过开头的空白符
*
* @param str 读入的内容
* @param n 字符串的内容
* @return 读入字符的个数
*/

int read_line(char str[], int n) {
// ch的类型为int而不是char,便于判定EOF
int ch, i =0;
// 跳过所有空白符
while (isspace(ch = getchar()))
;
while (ch != '\n' && ch != EOF) {
if (i < n) {
str[i++] = ch;
}
ch = getchar();
}
str[i] = '\0';
return i;
}
invent.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
/**
* Maintains a parts database (array version)
*/

#include <stdio.h>
#include "readline.h"

#define NAME_LEN 25
#define MAX_PARTS 100

/**
* 零件
*/

struct part {
int number;
char name[NAME_LEN + 1];
int on_hand;
} inventory[MAX_PARTS];

int num_parts = 0; // 当前零件的数量

int find_part (int number);

void insert(void);
void search(void);
void update(void);
void print(void);

// 循环等待用户操作
int main () {
char code;
for (;;) {
printf("Enter operation code:");
scanf(" %c", &code);

// 跳过所有的换行符
while (getchar() != '\n')
;
switch (code) {
case 'i':
insert();
break;
case 's':
search();
break;
case 'u':
update();
break;
case 'p':
print();
break;
case 'q':
return 0;
default:
printf("Illegal code\n");
}
printf("\n");
}
}

/**
* 按照零件的编号查找零件在清单数组中的下标
* @param number 零件的编号
* @return 零件在清单中的下标
*/

int find_part (int number) {
int i;
for (i = 0; i < num_parts; i++) {
if (inventory[i].number == number) {
return i;
}
}
return -1;
}

/**
* 通过命令行插入零件
*/

void insert (void) {
// 输入零件号
int part_number;
if (num_parts == MAX_PARTS) {
printf("Datebase is full, can't add more parts .\n");
return;
}
printf("Enter partnumber: ");
scanf("%d", &part_number);
if (find_part(part_number) >= 0) {
printf("Part already exists.\n");
return;
}
inventory[num_parts].number = part_number;

// 输入零件名
printf("Enter part name: ");
read_line(inventory[num_parts].name, NAME_LEN);
printf("Enter quantity on hand: ");
scanf("%d", &inventory[num_parts].on_hand);
num_parts++;
}

/**
* 在命令行根据零件编号搜索零件
*/

void search (void) {
int i, number;
printf("Enter part number: ");
scanf("%d", &number);
i = find_part(number);
if (i >= 0) {
printf("Part name: %s\n", inventory[i].name);
printf("Quantity on hand: %d\n", inventory[i].on_hand);
}
else {
printf("Part not found.\n");
}
}

/**
* 更新清单中某种零件的数量
*/

void update(void) {
int i, number, change;
printf("Enter part number : ");
scanf("%d", &number);
i = find_part(number);
if (i >= 0) {
printf("Enter change in quantity on hand: ");
scanf("%d", &change);
inventory[i].on_hand += change;
}
else {
printf("Part not found.\n");
}
}

/**
* 打印当前零件清单中所有种类零件的信息
*/

void print (void) {
int i;
printf("Part Number Part Name "
"Quantity on hand\n");
for (i = 0; i < num_parts; i++) {
printf("%7d %-25s%11d\n", inventory[i].number, inventory[i].name, inventory[i].on_hand);
}
}

16.3.4.2 编译

$ vim makefile

1
2
3
4
5
6
invent:invent.o readline.o
gcc -o invent invent.o readline.o
invent.o:invent.c readline.h
gcc -c invent.c
readline.o:readline.c readline.h
gcc -c readline.c

$ make

16.3.4.3 运行

1
2
3
4
5
6
7
8
9
$ ./invent 
Enter operation code:i
Enter partnumber: 01
Enter part name: screen
Enter quantity on hand: 1

Enter operation code:p
Part Number Part Name Quantity on hand
1 screen

16.4 联合

特点(和结构相比)

相同点

  • 包含一个或多个成员
  • 成员可以是不同的类型
  • 声明标记和类型的方式
  • 访问成员的方式
  • 可以使用=进行复制操作
  • 可以在函数间传递或作为函数的返回值
  • 初始化方式

不同点

  • 联合的实例所有成员共享相同的存储空间
  • 联合的实例大小由最大的成员的类型决定
  • 联合初始化实例时初始化的是按照第一个成员的类型来初始化值的

Alt text

1
2
3
4
5
6
union {
int i;
float f;
} u = {0}; // 0会按照i的类型初始化存储空间

u.f = 78.4; // 为联合赋值

16.4.1 使用联合来节省空间

原理:struct中使用union作为成员,后者使用struct作为成员。这种混合的结构可以实现一种数据结构应用于多种情境的效果。
扩展:c++中,struct中的union可以匿名,在c中则不得不指定union的名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <string.h>

#define TITLE_LEN 20
#define AUTHOR_LEN 10
#define DESIGN_LEN 10

/**
* 礼品册上的商品
* 可以存储3种类型的商品:书籍、杯子、衬衫
*
* @type {struct}
*/

struct catalog_item {
int stock_number; // 编号
float price; // 价格
int item_type; // 分类
union {
// 可能是书
struct {
char title[TITLE_LEN + 1];
char author[AUTHOR_LEN + 1];
int num_pages;
} book;

// 可能是杯子
struct {
char design[DESIGN_LEN + 1];
} mug;

// 可能是衬衫
struct {
char design[DESIGN_LEN + 1];
int colors;
int sizes;
} shirt;
} item;
};

int main () {
// 声明结构体实例
struct catalog_item bookItem;

// 为结构体中的联合的成员赋值
strcpy(bookItem.item.book.title, "three body");

// 访问结构题中的联合的成员
printf("%s\n", bookItem.item.book.title);
}

16.4.2 使用联合来构造混合的数据结构

说明:创建含有不同数据类型的混合数据结构(比如数组)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

/**
* 混合数据类型,包含整型和浮点型
*
* @typedef {union} Number
*/

typedef union {
int i;
float f;
} Number;

int main () {
// 声明混合型数组
Number number_array[1000];

// 赋值
number_array[0].i = 5; // 第一个值为整型
number_array[1].f = 3.14; // 第二个值为浮点型

// 访问
printf("%d\n", number_array[0].i); // 5

return 0;
}

16.4.3 为联合添加“标记字段”

用途:为联合提供额外的当前类型信息,防止获取到无意义的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>

#define INT_KIND 0
#define FLOAT_KIND 1

/**
*
* @typedef {struct}
*/

typedef struct {
int kind; // 标记字段
union {
int i;
float f;
} u;
} Number;

/**
* 根据数据结构的类型以不同的方式打印值
*
* @param {struct} n 要打印的数据结构
*/

void print_number(Number n);

int main() {
// 声明结构实例
Number n;

// 标记字段
n.kind = INT_KIND;

// 赋值
n.u.i = 82;

// 打印出来
print_number(n); // 82
}

void print_number(Number n) {
if (n.kind == INT_KIND) {
if (n.kind == INT_KIND) {
printf("%d\n", n.u.i);
}
else {
printf("%g\n", n.u.f);
}
}
}

16.5 枚举

说明:enum是一种由程序员列出值的类型,而且程序员必须为每种值(枚举常量)命名。
特点:

  • 遵循c语言的作用域规则(如果枚举声明在函数体内,那么它的常量对外部函数是不可见的)
  • 声明的方式类似结构和联合
1
2
// 最简单的声明方式:定义枚举类型的同时声明枚举变量
enum {CLUBS, DIAMONDS, HEARTS, SPADES} s1, s2;

16.5.1 枚举标记和枚举类型

说明:类似结构和联合的标记,有两种方式。

方式1:enum 标记名 {可能值}
1
2
3
4
5
// 创建枚举类型
enum suit {CLUBS, DIAMONDS, HEARTS, SPADES};

// 声明枚举变量
enum suit s1, s2;
方式2:typedef enum {可能值} 类型名

技巧:利用typedef来创建布尔类型是非常好的一种方法。

1
2
typedef enum {CLUBS, DIAMONDS, HEARTS, SPADES} Suit;
Suit s1, s2;

16.5.2 枚举作为整数

说明:在系统内部,c语言会把枚举变量和常量作为整数处理。

  • 当没有为枚举常量指定值时,它的值是一个大于前一个常量的值(默认第一个枚举常量的值为0)
  • 可以为枚举常量自由选择不同的值
  • 当为枚举常量指定值时,对大小顺序没有要求
  • 两个或多个枚举常量具有相同的值也是合法的
1
enum suit {CLUBS = 20, DIAMONDS = 10, HEARTS, SPADES}; // 20, 10, 11

扩展:把整数用作枚举的值是非常危险的,c++不允许整数用作枚举的值来使用。

1
2
3
4
5
6
int i;
enum suit {CLUBS, DIAMONDS, HEARTS, SPADES} s;
i = DIAMONDS;
s = 0;
s++;
i = s + 2;

16.5.3 用枚举声明“标记字段”

说明:enumunion配合实现union的“标记字段”。
优点:

  • 不需要额外定义宏
  • 明确类型的可能值范围
  • 含义更明确
1
2
3
4
5
6
7
typedeg struct {
enum {INT_KIND, FLOAT_KIND} kind;
union {
int i;
float f;
} u;
} Number;